250312 OpenGL学习记录

前言

寒假期间看了这个视频,被带着敲了一遍代码,这才有了“确实了解了一点OpenGL”的实感。

有些事情不上手去做,很难有底气断言“我已经掌握了/它很简单”

甚至有时候就算实际做过了某件事,也不能笃定自己对它知根知底。这篇分享对于我来说大概就算这样的东西。

我在那段时间学到的大概有:

这篇分享的内容大部分都是代码,偏向于记录性质,把上面那个视频十多小时的内容摘取出来了一部分。

环境与依赖

新建项目

VS2022创建C++空项目

项目下新建三个文件夹:

GLFW

https://www.glfw.org/

Download->64-bit Windows binaries

在3rdparty下新建文件夹GLFW,将解压后的include与lib-vc2022文件夹复制到3rdparty/GLFW下

由于不使用dll,删除3rdparty/GLFW/lib-vc2022下的glfw3.dll与glfw3dll.lib

Documentation->将Example code复制到main.cpp中

此时这段代码看起来非常喜庆:

右键单击项目->属性->配置属性->C/C++->常规->附加包含目录->输入src;3rdparty/GLFW/include

链接失败

配置属性->链接器->常规->附加库目录->输入3rdparty\GLFW\lib-vc2022

链接器->输入->附加依赖项->添加OpenGL32.lib;glfw3.lib;glfw3_mt.lib;

再次运行,此时应该可以看到这样的界面:

GLEW

https://glew.sourceforge.net/

Downloads->Binaries

直接把解压后的文件夹扔进3rdparty文件夹中,将它重命名为GLEW

右键单击项目->属性->配置属性->C/C++->常规->附加包含目录->添加3rdparty/GLEW/include

配置属性->链接器->常规->附加库目录->添加3rdparty\GLEW\lib\Release\x64

链接器->输入->附加依赖项->添加glew32s.lib

C/C++->预处理器->预处理器定义->添加GLEW_STATIC

对代码进行一点改动:

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <iostream>

int main(void)
{
    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit())
        return -1;
    
    // 设置OpenGL版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    glfwSwapInterval(1); // 设置刷新速率

    if (glewInit() != GLEW_OK) // 初始化GLEW
        std::cout << "ERROR!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl; // 打印OpenGL版本以便调试
    
    glEnable(GL_DEPTH_TEST); // 开启深度测试
    glEnable(GL_BLEND); // 开启混合
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    // 前者是原颜色比重,后者是目标颜色比重
    // 若前者a = 0.8,后者a = 0.4,则最终颜色从前者取0.8,后者取0.6

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 追加清理深度缓冲

        /* Swap front and back buffers */
        glfwSwapBuffers(window);

        /* Poll for and process events */
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

画一个三角形

顶点信息

假设红色边框是窗口范围,那么上下左右分别为(0, 1)(0, -1)(-1, 0)(1, 0)

故在初始化完毕,绘制开始前,设置如下三个顶点:

// ...
std::cout << glGetString(GL_VERSION) << std::endl; // 打印OpenGL版本以便调试

float vertex[] = {
    -0.5f,  0.5f
    -0.5f, -0.5f
     0.5f, -0.5f
}; // x, y

/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
// ...

rgb范围通常设置在[0, 1]内,对应[0, 255]。为了绘制一个kirakiradokidoki的三角形,我们给顶点添加上rgb:

float vertex[] = {
    -0.5f,  0.5f, 1, 0, 0,
    -0.5f, -0.5f, 0, 1, 0,
     0.5f, -0.5f, 0, 0, 1,
}; // x, y, r, g, b

顶点缓冲区

我们需要让显卡拿到这些数据

unsigned int vbo; // vertex buffer object
glGenBuffers(1, &vbo); // 申请一个缓冲区,获取它的句柄
glBindBuffer(GL_ARRAY_BUFFER, vbo); // 声明这个缓冲区为ARRAY_BUFFER
/* 将vertex中的数据传到缓冲区,并且提示这个缓冲区的用法为STATIC_DRAW
相应的,这个用法还有DYNAMIC_DRAW与STREAM_DRAW,用法可参考下面的官方文档 */
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);

STREAM You should use STREAM_DRAW when the data store contents will be modified once and used at most a few times.

STATIC Use STATIC_DRAW when the data store contents will be modified once and used many times.

DYNAMIC Use DYNAMIC_DRAW when the data store contents will be modified repeatedly and used many times.

顶点数组缓冲区

虽然我们知道每个顶点有五个float数据,前两个对应位置,后三个对应rgb,但是显卡不知道。所以我们需要告诉显卡这个数据如何布局。

unsigned int vao; // vertex array object
glGenVertexArrays(1, &vao); // 申请一个顶点数组,获取它的句柄
glBindVertexArray(vao); // 将opengl的数组缓冲区绑定上这个数组缓冲区

接下来有两个重要函数:

  1. glEnableVertexAttribArray(GLuint index)
    1. index:激活第几组数据
  2. glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void * pointer)
    1. index: 设置第几组数据
    2. size:该组包含几个数据
    3. type:数据类型
    4. normalized:是否归一化

      For ​**glVertexAttribPointer**​, specifies whether fixed-point data values should be normalized (GL_TRUE) or converted directly as fixed-point values (GL_FALSE) when they are accessed

    5. stride:步长,一个顶点包含多少字节数据
    6. pointer:这组数据的第一个字节是这个顶点数据的第几个字节

      Specifies a offset of the first component of the first generic vertex attribute in the array in the data store of the buffer currently bound to the GL_ARRAY_BUFFER target. The initial value is 0.

GL开头的那些类型基本只是些类型别名,并且可以往往可以一眼看出来,例如GLuint对应unsigned int。

对于我们x, y, r, g, b的布局,可以按照如下方式设置:

glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * 4, 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * 4, (const void*)8);

索引信息

试想我们要画的不止一个三角形,而是两个三角形构成的矩形,那么我们是否需要设置6个顶点共6*5*4=120字节的数据?

可以,但没必要,我们可以设置4个顶点6个索引共4*5*4+6*4=104字节的数据。随着三角形数量增多,优化效果会越来越明显。

以我们要画的三角形为例,这个三角形使用第0,1,2个顶点,故索引如下:

unsigned int index[] = {
    0, 1, 2,
};

索引缓冲区

基本可类比顶点缓冲区

unsigned int ibo;
glGenBuffers(1, &ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index), index, GL_STATIC_DRAW);

Shader

这里我们的重点是Vertex Shader与Fragment Shader。

为了便于组织,我们将两份shader放进一个文件中,命名为Basic.shader。

新建res/shaders文件夹,将Basic.shader置于其下。

#shader vert // 这不是glsl的语法,这只是便于自己处理文本识别Shader类型
#version 330 core

layout(location = 0) in vec4 position;
layout(location = 1) in vec4 color;

out vec4 v_Color;

void main(){
    gl_Position = position;
    v_Color = color;
}

#shader frag
#version 330 core

in vec4 v_Color;

out vec4 o;

void main(){
    o = v_Color;
}

文件读取

添加头文件

#include <fstream>
#include <sstream>

这里主要是要认出自己写的#shader vert/frag

struct ShaderProgramSource {
    std::string Vert;
    std::string Frag;
};

ShaderProgramSource ParseShader(const std::string& path) {
    std::ifstream fs(path);
    std::stringstream ss[2];
    enum { None = -1, Vert = 0, Frag = 1 }type = None;
    std::string line;
    while (getline(fs, line))
        if (line.find("#shader") != std::string::npos)
            if (line.find("vert") != std::string::npos) type = Vert;
            else if (line.find("frag") != std::string::npos) type = Frag;
            else type = None;
        else ss[(int)type] << line << '\n';
    return { ss[0].str(),ss[1].str() };
}

使用Shader

在main函数中调用这些方法

ShaderProgramSource source(ParseShader("res/shaders/Basic.shader"));
unsigned int shader = CreateShader(source.Vert, source.Frag);

绘制

做了那么多准备,我们终于可以开始绘制了!

这一步其实很简单,只需要在while循环里添加一段代码:

// ...
/* Render here */
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glUseProgram(shader); // 指定使用的shader程序
glBindVertexArray(vao); // 绑定顶点数组
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); // 绑定索引缓冲区
glDrawElements(GL_TRIANGLES, sizeof(index) / 4, GL_UNSIGNED_INT, nullptr);
// 绘制,这就是我们常说的draw call

/* Swap front and back buffers */
glfwSwapBuffers(window);
// ...

glDrawElements(GLenum mode, GLsizei count, GLenum type, const void * indices)

如果没出意外的话,运行程序,你已经可以绘制一个三角形了!

让三角形动起来

想要让三角形动起来,我们可以每一帧修改一次顶点缓冲区的数据,但这样实在有点麻烦。有没有更加简单高效的实现方法呢?

有的兄弟,有的,这样的方法多得数不清。记得Unity里面,我们可以通过Shader.PropertyToId(string)获取shader中一个属性的id,再通过Material.SetXXX(int, ...)修改这个属性的值;这里也有对应的方法。

首先在Basic.shader中设置uniform变量:

#shader vert
#version 330 core

layout(location = 0) in vec4 position;
layout(location = 1) in vec4 color;

uniform vec2 u_Offset;

out vec4 v_Color;

void main(){
        gl_Position = position + vec4(u_Offset, 0, 0);
        v_Color = color;
}
// ...

然后在main函数的while循环里改动这个变量:

// ...
glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, 0);

static float offset = 0, increment = 0.02f; 
// static修饰符保证仅初始化这一次。主要还是为了演示才这么做,正常写建议放循环前面去
offset += increment;
if (offset > 0.5f || offset < -0.5f) increment = -increment;

static int offsetLocation = glGetUniformLocation(shader, "u_Offset"); // 获取id
glUniform2f(offsetLocation , offset, 0); // 设置变量

/* Swap front and back buffers */
glfwSwapBuffers(window);
// ...

现在这个三角形学会反复横跳了!

代码参考如下:

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <iostream>
#include <fstream>
#include <sstream>

struct ShaderProgramSource {
    std::string Vert;
    std::string Frag;
};

ShaderProgramSource ParseShader(const std::string& path) {
    std::ifstream fs(path);
    std::stringstream ss[2];
    enum { None = -1, Vert = 0, Frag = 1 }type = None;
    std::string line;
    while (getline(fs, line))
        if (line.find("#shader") != std::string::npos)
            if (line.find("vert") != std::string::npos) type = Vert;
            else if (line.find("frag") != std::string::npos) type = Frag;
            else type = None;
        else ss[(int)type] << line << '\n';
    return { ss[0].str(),ss[1].str() };
}

unsigned int CompileShader(unsigned int type, const std::string& source) {
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str();
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result);
    if (result == GL_FALSE) {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
        char* message = (char*)alloca(length * sizeof(char));
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Fail to compile " <<
            (type == GL_VERTEX_SHADER ? "vertex" : "fragment")
            << "shader!" << std::endl << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader) {
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);
    glValidateProgram(program);

    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

int main(void)
{
    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit())
            return -1;

    // 设置OpenGL版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);
    
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glfwSwapInterval(1); // 设置刷新速率

    if (glewInit() != GLEW_OK) // 初始化GLEW
        std::cout << "ERROR!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl; // 打印OpenGL版本以便调试

    float vertex[] = {
        -0.5f,  0.5f, 1, 0, 0,
        -0.5f, -0.5f, 0, 1, 0,
         0.5f, -0.5f, 0, 0, 1,
    }; // x, y, r, g, b
    unsigned int vbo;
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);

    unsigned int vao;
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * 4, 0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * 4, (const void*)8);

    unsigned int index[] = {
        0, 1, 2,
    };
    unsigned int ibo;
    glGenBuffers(1, &ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index), index, GL_STATIC_DRAW);

    ShaderProgramSource source(ParseShader("res/shaders/Basic.shader"));
    unsigned int shader = CreateShader(source.Vert, source.Frag);

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(shader);
        glBindVertexArray(vao);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
        glDrawElements(GL_TRIANGLES, sizeof(index) / 4, GL_UNSIGNED_INT, 0);
        
        static float offset = 0, increment = 0.02f;
        offset += increment;
        if (offset > 0.5f || offset < -0.5f) increment = -increment;
        
        static int location = glGetUniformLocation(shader, "u_Offset");
        glUniform2f(location, offset, 0);

        /* Swap front and back buffers */
        glfwSwapBuffers(window);

        /* Poll for and process events */
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

封装

为了便于复用,提升可读性,我们可以将这些代码封装起来。

IndexBuffer

在src文件夹下创建IndexBuffer类(IndexBuffer.cpp&IndexBuffer.h)

注意到我们在创建索引缓冲区绘制三角形时均调用了相关api,故提取出构造函数和Bind两个方法;顺道把Unbind也一块儿写了,还要记得析构防止内存泄漏;draw call需要知道​索引数​,故应该对外暴露Count属性。

.h

#pragma once
class IndexBuffer
{
private:
    unsigned int m_RendererID;
    unsigned int m_Count;
    unsigned int m_Usage;
public:
    IndexBuffer(const unsigned int* data, unsigned int count, unsigned int usage);
    ~IndexBuffer();

    void Bind() const;
    void Unbind() const;

    inline unsigned int GetCount() const { return m_Count; }
    // 与第6行结合起来看,其实就是C#里面的public uint Count { get; private set; }
};

.cpp

#include "IndexBuffer.h"

#include <GL/glew.h>
#include <GLFW/glfw3.h>

IndexBuffer::IndexBuffer(const unsigned int* data, unsigned int count, unsigned int usage)
    :m_Count(count), m_Usage(usage)
{
    glGenBuffers(1, &m_RendererID);
    Bind();
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(unsigned int), data, usage);
}

IndexBuffer::~IndexBuffer()
{
    glDeleteBuffers(1, &m_RendererID);
}

void IndexBuffer::Bind() const
{
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID);
}

void IndexBuffer::Unbind() const
{
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

VertexBuffer

在src文件夹下创建VertexBuffer类(VertexBuffer.cpp&VertexBuffer.h)

大致可参照IndexBuffer,甚至由于无需暴露m_Count,工作量还要小一些;然而配套工作不少。

.h

#pragma once
class VertexBuffer
{
private:
    unsigned int m_RendererID;
    unsigned int m_Usage;
public:
    VertexBuffer(const void* data, unsigned int size, unsigned int usage);
    ~VertexBuffer();

    void Bind() const;
    void Unbind() const;
};

.cpp

#include "VertexBuffer.h"

#include <GL/glew.h>
#include <GLFW/glfw3.h>

VertexBuffer::VertexBuffer(const void* data, unsigned int size, unsigned int usage)
    :m_Usage(usage)
{
    glGenBuffers(1, &m_RendererID);
    Bind();
    glBufferData(GL_ARRAY_BUFFER, size, data, usage);
}

VertexBuffer::~VertexBuffer()
{
    glDeleteBuffers(1, &m_RendererID);
}

void VertexBuffer::Bind() const
{
    glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
}

void VertexBuffer::Unbind() const
{
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

VertexArray

在src文件夹下创建VertexArray类(VertexArray.cpp&VertexArray.h)

它的构造、析构、绑定、解绑均可参照VertexBuffer

#pragma once
class VertexArray
{
private:
    unsigned int m_RendererID;
public:
    VertexArray();
    ~VertexArray();

    void Bind() const;
    void Unbind() const;
};

然而要描述顶点的内存布局,我们还需要一点额外的工作

以一开始的vertex数组为例,它的内存布局大概可以这么划分:

接下来就让我们把这段零散的布局组织起来吧!

Layout&Element

在src文件夹下创建VertexBufferLayout.cpp&VertexBufferLayout.h

由于VertexBufferElement内容并不多,可以将它写在刚刚创建的两个文件中

.h

#pragma once

#include <vector>

struct VertexBufferElement {
    unsigned int type, count;
    unsigned char normalized;
    // 以上均与glVertexAttribPointer方法中的参数对应

    static unsigned int GetSizeOfType(unsigned int type);
    // 像sizeof一样获取GLenum的大小
};

class VertexBufferLayout
{
private:
    std::vector<VertexBufferElement> m_Elements;
    unsigned int m_Stride;
    // 对应glVertexAttribPointer方法中的步长,也因此需提供getter(即GetStride)
public:
    VertexBufferLayout();

    inline const std::vector<VertexBufferElement> GetElements() const { return m_Elements; }
    inline unsigned int GetStride() const { return m_Stride; }

    template<typename T>
    void Push(unsigned int count);
    template<>
    void Push<float>(unsigned int count);
    template<>
    void Push<unsigned int>(unsigned int count);
    template<>
    void Push<unsigned char>(unsigned int count);
};

.cpp

#include "VertexBufferLayout.h"

#include <GL/glew.h>
#include <GLFW/glfw3.h>

unsigned int VertexBufferElement::GetSizeOfType(unsigned int type)
{
    switch (type)
    {
    case GL_FLOAT:
    case GL_UNSIGNED_INT:
        return 4;
    case GL_UNSIGNED_BYTE:
        return 1;
    }
    return 0;
}

VertexBufferLayout::VertexBufferLayout()
    :m_Stride(0) {}

// 以下是全特化模板的定义,据说有的编译器不支持把它和声明的文件分开,故建议放在头文件里
// 不过it works on my machine(
template<typename T>
void VertexBufferLayout::Push(unsigned int count) { }
template<>
void VertexBufferLayout::Push<float>(unsigned int count)
{
    m_Elements.push_back({ GL_FLOAT, count, GL_FALSE });
    m_Stride += count * VertexBufferElement::GetSizeOfType(GL_FLOAT);
}
template<>
void VertexBufferLayout::Push<unsigned int>(unsigned int count)
{
    m_Elements.push_back({ GL_UNSIGNED_INT, count, GL_FALSE });
    m_Stride += count * VertexBufferElement::GetSizeOfType(GL_UNSIGNED_INT);
}
template<>
void VertexBufferLayout::Push<unsigned char>(unsigned int count)
{
    m_Elements.push_back({ GL_UNSIGNED_BYTE, count, GL_TRUE });
    m_Stride += count * VertexBufferElement::GetSizeOfType(GL_UNSIGNED_BYTE);
}

然后在VertexArray类中添加一个方法来应用布局

void AddBuffer(const VertexBuffer& vb, const VertexBufferLayout& layout) const
{
    Bind();
    vb.Bind();
    const auto& elements = layout.GetElements();
    for (unsigned int i = 0, offset = 0; i < elements.size(); i++) {
        const auto& element = elements[i];
        glEnableVertexAttribArray(i);
        glVertexAttribPointer(i, element.count, element.type, element.normalized, layout.GetStride(), (const void*)offset);
        offset += element.count * VertexBufferElement::GetSizeOfType(element.type);
    }
}

Shader

在src文件夹下创建Shader类(Shader.cpp&Shader.h)

除开常见的构造、析构、绑定、解绑之外,这里需要注意的是让三角形动起来一节中提到的几个对标Unity(倒反天罡)的函数,开箱即用的现成方法也不少。

.h

#pragma once

#include <string>

struct ShaderProgramSource {
    std::string Vert;
    std::string Frag;
};

class Shader
{
private:
    unsigned int m_RendererID;
    std::string m_FilePath;
public:
    Shader(const std::string& path);
    ~Shader();

    void Bind() const;
    void Unbind() const;
    
    // Shader.PropertyToId(string)
    int GetUniformLocation(const std::string& name);
    
    // 有一堆形如glUniformXX(GLint, ...)的方法,这里按需添加相应函数即可
    void SetUniform1i(int location, int v0);
    void SetUniform2f(int location, float v0, float v1);
private:
    // 从main.cpp里面拿来就好了,基本不需要改动
    unsigned int CompileShader(unsigned int type, const std::string& source);
    unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader);
    ShaderProgramSource ParseShader(); // 只有这里的path被换成了m_FilePath
};

.cpp

#include "Shader.h"

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <iostream>
#include <fstream>
#include <sstream>

Shader::Shader(const std::string& path)
    :m_RendererID(0), m_FilePath(path)
{
    ShaderProgramSource source(ParseShader());
    m_RendererID = CreateShader(source.Vert, source.Frag);
}

Shader::~Shader()
{
    glDeleteProgram(m_RendererID);
}

void Shader::Bind() const
{
    glUseProgram(m_RendererID);
}

void Shader::Unbind() const
{
    glUseProgram(0);
}

int Shader::GetUniformLocation(const std::string& name)
{
    int location = glGetUniformLocation(m_RendererID, name.c_str());
    if (location == -1)
        std::cout << "Warning: uniform \"" << name << "\" doesn't exist!" << std::endl;
    return location;
}

void Shader::SetUniform1i(int location, int v0)
{
    glUniform1i(location, v0);
}

void Shader::SetUniform2f(int location, float v0, float v1)
{
    glUniform2f(location, v0, v1);
}

unsigned int Shader::CompileShader(unsigned int type, const std::string& source)
{
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str();
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result);
    if (result == GL_FALSE) {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
        char* message = (char*)alloca(length * sizeof(char));
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Fail to compile " <<
            (type == GL_VERTEX_SHADER ? "vertex" : "fragment")
            << "shader!" << std::endl << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

unsigned int Shader::CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);
    glValidateProgram(program);

    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

ShaderProgramSource Shader::ParseShader()
{
    std::ifstream fs(m_FilePath);
    std::stringstream ss[2];
    enum { None = -1, Vert = 0, Frag = 1 }type = None;
    std::string line;
    while (getline(fs, line))
        if (line.find("#shader") != std::string::npos)
            if (line.find("vert") != std::string::npos) type = Vert;
            else if (line.find("frag") != std::string::npos) type = Frag;
            else type = None;
        else ss[(int)type] << line << '\n';
    return { ss[0].str(),ss[1].str() };
}

假如我想通过一行shader.SetUniform2f("u_Offset", offset, 0)完成之前两行的效果,如何将这个重载实现得又快又好;或者说,假如我会多次查询同一个变量的id,如何保证后续查询的效率?

使用unordered_map<srting, int>做个缓存就好了

Renderer

在src文件夹下创建Renderer类(Renderer.cpp&Renderer.h)

这里和渲染相关的有glClear(GLbitfield(其实就是unsigned int))和draw call,可以把它们提取到Renderer

.h

#pragma once

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include "IndexBuffer.h"
#include "VertexArray.h"
#include "Shader.h"

class Renderer
{
public:
    static void Clear(unsigned int target = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    static void Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader);
};

.cpp

#include "Renderer.h"

void Renderer::Clear(unsigned int target)
{
    glClear(target);
}

void Renderer::Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader)
{
    shader.Bind();
    va.Bind();
    ib.Bind();
    glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, nullptr);
}

这个Renderer中的方法都是静态的,其实也可以写成实例方法

应用

将各种.cpp文件里面的#include <GL/glew.h>#include <GLFW/glfw3.h>替换成#include "Renderer.h",然后把main.cpp里面未封装的代码替换成封装好的代码,效果大致如下:

#include <iostream>

#include "Renderer.h"

int main(void)
{
    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit())
        return -1;

    // 设置OpenGL版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    glfwSwapInterval(1); // 设置刷新速率

    if (glewInit() != GLEW_OK) // 初始化GLEW
        std::cout << "ERROR!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl; // 打印OpenGL版本以便调试
    
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    float vertex[] = {
        -0.5f,  0.5f, 1, 0, 0,
        -0.5f, -0.5f, 0, 1, 0,
         0.5f, -0.5f, 0, 0, 1,
    }; // x, y, r, g, b
    std::unique_ptr<VertexBuffer> vb = 
        std::make_unique<VertexBuffer>(vertex, sizeof(vertex), GL_STATIC_DRAW);

    VertexBufferLayout layout;
    layout.Push<float>(2);
    layout.Push<float>(3);
    
    std::unique_ptr<VertexArray> va = std::make_unique<VertexArray>();
    va->AddBuffer(*vb, layout);

    unsigned int index[] = {
        0, 1, 2,
    };
    std::unique_ptr<IndexBuffer> ib = std::make_unique<IndexBuffer>
        (index, sizeof(index) / sizeof(unsigned int), GL_STATIC_DRAW);

    std::unique_ptr<Shader> shader = 
        std::make_unique<Shader>("res/shaders/Image.shader");

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        Renderer::Clear();

        Renderer::Draw(*va, *ib, *shader);

        static float offset = 0, increment = 0.02f;
        offset += increment;
        if (offset > 0.5f || offset < -0.5f) increment = -increment;

        static int offsetLocation = shader->GetUniformLocation("u_Offset");
        shader->SetUniform2f(offsetLocation , offset, 0);

        /* Swap front and back buffers */
        glfwSwapBuffers(window);

        /* Poll for and process events */
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

显示一张图片

画一个矩形

准备一个平平淡淡才是真的shader,将它命名为Image.shader

#shader vert
#version 330 core

layout(location = 0) in vec4 position;

uniform vec2 u_Offset;

void main(){
    gl_Position = position + vec4(u_Offset, 0, 0);
}

#shader frag
#version 330 core

out vec4 o;

void main(){
    o = vec4(1, 1, 1, 1);
}

vertex数组暂时只需包含四个顶点的xy:

float vertex[] = {
    -0.5f,  0.5f,
    -0.5f, -0.5f,
     0.5f, -0.5f,
     0.5f,  0.5f,
}; // x, y

index数组则需添加矩形另一半三角形的索引

unsigned int index[] = {
    0, 1, 2,
    2, 3, 0,
};

删除原先用于rgb的layout.Push<float>(3);

把初始化shader的字符串换成"res/shaders/Image.shader"

效果如下:

stb_image

https://github.com/nothings/stb/blob/master/stb_image.h

下载stb_image.h,在3rdparty文件夹下创建stb_image文件夹,把stb_image.h放进去,创建stb_image.cpp,内容如下:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

右键单击项目->属性->配置属性->C/C++->常规->附加包含目录->添加3rdparty

右键单击该文件夹,将它们包含在项目中

Texture

在src文件夹下创建Texture类(Texture.cpp&Texture.h)

接下来介绍几个函数:

  1. stbi_set_flip_vertically_on_load(int flag_true_if_should_flip)

    1. flag_true_if_should_flip:非0则垂直翻转图片

    对于一张图片的uv坐标,OpenGL以左下角为(0, 0),而加载图片时可能会从上往下加载,这意味着左上角为(0, 0),故y轴可能颠倒。此时就需要翻转图片。

  2. stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp)

    1. filename:文件路径
    2. x:图片宽度
    3. y:图片高度
    4. comp:图片颜色通道数
    5. req_comp:预期颜色通道数

    我们希望读取到RGBA四个通道,所以通常设置为4

    1. 返回读取到的内容,虽然具体是什么不清楚,但是OpenGL能用(
  3. glTexParameteri(GLenum target, GLenum pname, GLint param) 一个设置纹理各种处理细节的函数,有兴趣可以参阅https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexParameter.xhtml

  4. glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels)

    1. target:参考glTexParameteri的同名参数
    2. level:level of detail,这里设置为0
    3. border:一图胜千言: 我不理解为什么会给这样一个参数
    4. 其余参数可参阅https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml
  5. glActiveTexture(GLenum texture)

    1. texture:激活材质槽位,可填GL_TEXTURE0~GL_TEXTURE31,移动端往往只有8个孔位;不过这里暂不需要担心槽位不够。由于GL_TEXTURE0~GL_TEXTURE31与0x84C0~0x84DF一一对应,指定第slot个槽位其实只需要填写GL_TEXTURE0 + slot

.h

#pragma once

#include <string>

class Texture
{
private:
    unsigned int m_RendererID;
    std::string m_FilePath;
    unsigned char* m_LocalBuffer;
    int m_Width, m_Height, m_BPP;
public:
    Texture(const std::string& path, bool flip);
    ~Texture();

    void Bind(unsigned int slot = 0) const;
    void Unbind() const;

    inline int GetWidth() const { return m_Width; }
    inline int GetHeight() const { return m_Height; }
};

.cpp

#include "Texture.h"

#include "Renderer.h"

#include <stb_image/stb_image.h>

Texture::Texture(const std::string& path, bool flip)
    : m_RendererID(0), m_FilePath(path), m_LocalBuffer(nullptr)
    , m_Width(0), m_Height(0), m_BPP(0)
{
    stbi_set_flip_vertically_on_load(flip);
    m_LocalBuffer = stbi_load(path.c_str(), &m_Width, &m_Height, &m_BPP, 4);

    glGenTextures(1, &m_RendererID);
    Bind();

    // 图像放大缩小均采用线性过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // 图形包围方式设置为重复(平铺)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Width, m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_LocalBuffer);
    glGenerateMipmap(GL_TEXTURE_2D); // 生成mipmap

    if (m_LocalBuffer) stbi_image_free(m_LocalBuffer);
}

Texture::~Texture()
{
    glDeleteTextures(1, &m_RendererID);
}

void Texture::Bind(unsigned int slot) const
{
    glActiveTexture(GL_TEXTURE0 + slot);
    glBindTexture(GL_TEXTURE_2D, m_RendererID);
}

void Texture::Unbind() const
{
    glBindTexture(GL_TEXTURE_2D, 0);
}

应用

首先,在res文件夹下创建textures文件夹,在里面放一张图片。

我使用的是下面这张:

由于图片中的人物叫时坂琉璃,我将这张图片命名为tokisaka_ruri.png

其次是Image.shader,现在它需要知道它应该采样纹理的哪个​坐标​,还需要一个uniform变量指定采样第几张​纹理​:

#shader vert
#version 330 core

layout(location = 0) in vec4 position;
layout(location = 1) in vec2 texCoord;

uniform vec2 u_Offset;

out vec2 v_TexCoord;

void main(){
    gl_Position = position + vec4(u_Offset, 0, 0);
    v_TexCoord = texCoord;
}

#shader frag
#version 330 core

in vec2 v_TexCoord;

uniform sampler2D u_Texture;

out vec4 o;

void main(){
    o = texture(u_Texture, v_TexCoord); 
    // 采样图片某一坐标上的颜色,得到的是个vec4变量
}

然后是vertex数组,现在它需要给shader传输顶点的​uv坐标​:

float vertex[] = {
    -0.5f,  0.5f, 0, 1,
    -0.5f, -0.5f, 0, 0,
     0.5f, -0.5f, 1, 0,
     0.5f,  0.5f, 1, 1,
}; // x, y, u, v

相应的,layout也应该再Push两个float数据进去

shader附近应用Texture,并设置uniform变量:

std::unique_ptr<Texture> texture = std::make_unique<Texture>("res/textures/tokisaka_ruri.png", true);
std::unique_ptr<Shader> shader = std::make_unique<Shader>("res/shaders/Image.shader");

int textureLocation = shader->GetUniformLocation("u_Texture");
shader->SetUniform1i(textureLocation, 0);

还能把窗口大小从(640, 480)调成(960, 540),不然比例不对

效果如下: